6.7 Vererbung 

Neben der Assoziation von Objekten gibt es in der Objektorientierung eine weitere wichtige Möglichkeit zur Wiederverwendung: die Vererbung. Sie basiert auf der Idee, dass Eltern ihren Kindern Eigenschaften mitgeben. Vererbung bindet die Klassen noch dichter aneinander. Mittels dieser engen Verbindung können wir später sehen, dass Klassen in gewisser Weise austauschbar sind.
| Hinweis In Java können nur Untertypen von Klassen deklariert werden. Einschränkungen von primitiven Typen – etwa im Wertebereich oder in der Anzahl der Nachkommastellen – sind nicht möglich. Die Programmiersprache Ada erlaubt das zum Beispiel, und Untertypen sind bei XML-Schema üblich, wo etwa xs:short oder xs:unsignedByte ein Untertyp von xs:integer sind. |
6.7.1 Vererbung in Java 

Die Klassen in Java sind in einer Hierarchie geordnet. Von Object erben automatisch alle Klassen, direkt oder indirekt. Eine neu deklarierte Klasse erweitert durch das Schlüsselwort extends eine andere Klasse. Sie wird dann zur Unterklasse (auch Subklasse oder Kindklasse genannt). Die Klasse, von der die Unterklasse erbt, heißt Oberklasse (auch Superklasse oder Elternklasse). Durch den Vererbungsmechanismus werden alle sichtbaren Eigenschaften der Oberklasse auf die Unterklasse übertragen. Eine Oberklasse vererbt also Eigenschaften, und die Unterklasse erbt sie.
Syntaktisch wird die Vererbung durch das Schlüsselwort extends beschrieben. Allgemein gilt für eine erbende Klasse Unter und eine Oberklasse Ober:
class Unter extends Ober { }
Da die Mehrfachvererbung in Java nicht gültig ist, steht hinter dem Schlüsselwort extends lediglich eine einzige Klasse.
Alles, was nun Ober an sichtbaren Eigenschaften besitzt, wird auf Unter vererbt. Die Klasse Unter kann die vererbten Eigenschaften nutzen. Wenn sich die Implementierung einer Methode der Oberklasse ändert, wird die Unterklasse diese Änderung mitbekommen.
6.7.2 Einfach- und Mehrfachvererbung 

In Java ist auf direktem Weg nur die Einfachvererbung (engl. single inheritance) erlaubt. In der Einfachvererbung kann eine Klasse lediglich eine andere erweitern. In Programmiersprachen wie C++ können auch mehrere Klassen zu einer neuen verbunden werden. Dies bringt aber einige Probleme mit sich, die in Java vermieden werden. Nehmen wir an, die Klassen O1 und O2 deklarieren beide eine öffentliche Funktion f(), und U ist eine Klasse, die von O1 und O2 erbt. Steht in U ein Funktionsaufruf f(), ist nicht klar, welche der beiden Funktionen gemeint ist. In C++ löst der Scope-Operator (::) das Problem, in dem der Entwickler immer angibt, aus welcher Oberklasse die Funktion anzusprechen ist.
Dazu gesellt sich das Diamanten-Problem (auch Rauten-Problem genannt). Zwei Klassen K1 und K2 erben von einer Oberklasse O eine Eigenschaft x. Eine Unterklasse U erbt von den Klassen K1 und K2. Lässt sich in U auf die Eigenschaft x zugreifen? Eigentlich existiert die Eigenschaft ja nur einmal und dürfte kein Grund zur Sorge sein. Dennoch stellt dieses Szenario ein Problem dar, weil der Compiler »vergessen« hat, dass sich x in den Unterklassen K1 und K2 nicht verändert hat; mit der Einfachvererbung kommt es erst gar nicht zu diesem Dilemma.
6.7.3 Gebäude modelliert 

Wir wollen nun eine Klassenhierarchie für Gebäude aufbauen. Die Hierarchie geht von oben nach unten, von der Oberklasse zur Unterklasse. Im Fall einer Diskothek ist diese ein Gebäude, sodass schon die erste Ist-eine-Art-von-Hierarchie existiert. Eine Diskothek ist eine Art Gebäude. Denkbar wären auch spezielle Diskotheken, etwa Kinderdiskotheken. Wir können auch dann sagen: Eine Kinderdisko ist eine spezielle Art von Diskothek.
Schreiben wir die Hierarchie für Kinderdiskotheken auf. Der Quellcode der Oberklasse Disko muss dazu nicht geändert werden. Das ist typisch für die Modellierung mit Klassenhierarchien – die Oberklasse weiß von einer Unterklasse gar nichts!
Beginnen wir mit der Basisklasse Disko.
public class Disko { ... }
Da keine ausdrückliche extends-Anweisung hinter dem Klassennamen steht, erbt die Klasse automatisch von Object, einer impliziten Basisklasse. Das ist jetzt nicht sonderlich spannend, aber KinderDisko wird interessant:
Listing 6.34 v7/KinderDisko.java
public class KinderDisko extends Disko { public String maskottchen; }
Die Deklaration der Klasse trägt den Anhang extends Disko und erbt somit alle sichtbaren Eigenschaften der Oberklasse. Sie selbst fügt der Klasse nur ein Attribut maskottchen zu.
Damit ergibt sich das nachfolgende UML-Diagramm. Die Vererbung ist durch einen Pfeil in Richtung der Oberklasse angegeben.
Abbildung 6.2 Eine Kinderdisko ist eine Spezialisierung einer Disko.
Haben wir ein KinderDisko-Objekt erzeugt, können wir auf alle Eigenschaften der Kinderdisko zugreifen, aber auch auf die Eigenschaften, die geerbt wurden.
Listing 6.35 Ausschnitt aus KiDiDemo.java
KinderDisko saloon = new KinderDisko(); saloon.maskottchen = "Yosemite Sam"; saloon.personRein();
6.7.4 Konstruktoren in der Vererbung 

Obwohl Konstruktoren Ähnlichkeit mit Methoden haben, etwa in der Eigenschaft, dass sie überladen werden oder Ausnahmen erzeugen können, werden sie im Gegensatz zu Methoden nicht vererbt. Das heißt, eine Unterklasse muss ganz neue Konstruktoren angeben, denn mit den Konstruktoren der Oberklasse kann ein Objekt der Unterklasse nicht erzeugt werden. Ob das nun reine Objektorientierung ist – darüber lässt sich streiten; in der Skriptsprache Python etwa werden auch Konstruktoren vererbt. In Java gehören Konstruktoren eigentlich zum statischen Teil einer Klasse. Die Klasse selbst weiß, wie neue Objekte konstruiert werden. Würden wir Konstruktoren eher als Initialisierungsmethoden ansehen, läge es natürlich näher, sie wie Objektmethoden zu behandeln. Dagegen spricht jedoch, dass eine Unterklasse mehr Eigenschaften hat und der Konstruktor der Oberklasse dann nur einen Teil initialisieren würde.
In Java sammelt eine Unterklasse zwar automatisch alle sichtbaren Eigenschaften der Oberklasse, aber die Objekte in der Hierarchie existieren einzeln. Das heißt: Wenn eine Unterklasse erzeugt wird, dann ruft der Konstruktor der Unterklasse automatisch den Standard-Konstruktor der Oberklasse auf, um das obere Objekt zu initialisieren. Es ist dabei egal, ob der Konstruktor in der Unterklasse parametrisiert ist oder nicht; jeder Konstruktor der Unterklasse muss einen der Oberklasse aufrufen.
Ein Beispiel mit Konstruktorweiterleitung
Sehen wir uns noch einmal die Konstruktorverkettung an:
class Gebaeude { } class Disko extends Gebaeude { }
Da wir keine expliziten Konstruktoren haben, fügt der Compiler zwei Standard-Konstruktoren ein. Sie rufen zudem den Standard-Konstruktor der Oberklasse auf. Daher ergibt sich das folgende Bild in den Klassen für die Laufzeitumgebung im Bytecode:
class Gebaeude { Gebaeude() { super(); // für Object() } } class Disko extends Gebaeude { Disko() { super(); // für Gebaeude() } }
Wir sehen, dass wir nicht ausdrücklich super() schreiben müssen, weil dies der Compiler übernimmt.
Ein unnötiges super() in der ersten Zeile?
In vielen Java-Programmen (auch in der Java-Klassenbibliothek, besonders bei den Ausnahmen) steht aber trotzdem super() in der ersten Zeile des Konstruktors – so zum Beispiel in der Klasse Vector:
public Vector( int initialCapacity, int capacityIncrement ) { super(); ... }
oder in der Klasse IOException:
public IOException() { super(); }
Wie wir gesehen haben, ist dies nicht notwendig, kann aber die Lesbarkeit fördern. Wir sind uns dann sofort bewusst, dass die »Methode« ein Konstruktor ist und dass der Standard-Konstruktor aufgerufen wird.
super() mit Argumenten füllen
Mitunter ist es nötig, aus der Unterklasse nicht nur den Standard-Konstruktor anzusteuern, sondern einen anderen (parametrisierten) Konstruktor der Oberklasse. Dazu kann ein super()-Aufruf mit Argumenten gefüllt werden. Gründe dafür könnten sein:
- Wenn wir keinen Standard-Konstruktor in der Oberklasse vorfinden, müssen wir in der Unterklasse mittels super(Argument ...) einen speziellen, parametrisierten Konstruktor aufrufen.
Dazu werfen wir noch einmal einen Blick auf die Implementierung von IOException, wo wir direkt die Zeichenkette weiter nach oben geben:
public class IOException extends Exception { public IOException() { super(); } public IOException( String s ) { super( s ); } }
6.7.5 Sichtbarkeit protected 

Die Vererbung kann durch private eingeschränkt werden. Eine Subklasse erbt dann alles von einer Superklasse, was nicht private ist. Zusätzlich kommt zu private noch eine Sonderform protected hinzu. Hier kann auch eine Unterklasse alle Eigenschaften sehen. Nur von außen sind die Eigenschaften privat. Eine Ausnahme bilden jedoch Klassen, die im gleichen Paket sind; auch sie können die Eigenschaften einer protected-Klasse sehen. Damit ist protected mehr als nur die Sichtbarkeit für Unterklassen, denn wenn auch Klassen im gleichen Paket lesen können, ist die Sichtbarkeit fast public. Nehmen wir eine Klasse K und L im Paket p an. Deklariert K die Attribute protected, so kann L diese lesen und modifizieren.
Stellen wir uns ein anderes Beispiel mit zwei Klassen aus zwei unterschiedlichen Paketen vor. Ein gemeinsames Oberpaket pakettest besitzt die beiden Unterpakete berlusconi und stefani mit den Klassen Silvio und Stefano.
Listing 6.36 pakettest.berlusconi.Silvio
package pakettest.berlusconi; public class Silvio { protected String schulz = "einförmiger, supernationalistischer Blonder"; }
Die Klasse Silvio deklariert lediglich eine geschützte Variable schulz. Sie wird also von allen Unterklassen und auch allen Klassen im gleichen Paket nutzbar sein. Wir deklarieren eine zweite Klasse Stefano jedoch in einem anderen Paket:
Listing 6.37 pakettest.stefani.Silvio
package pakettest.stefani; import pakettest.berlusconi.Silvio; public class Stefano extends Silvio { String übernommen = schulz; void demokratieverständnis( Silvio s ) { // s.schulz ist hier nicht deklariert } }
Obwohl eine Unterklasse die protected-Eigenschaft schulz nutzen kann (in übernommen = schulz ist ein Zugriff auf die Oberklassenvariable enthalten, kann sie nicht über den Typ Silvio auf schulz zugreifen.
6.7.6 Das Substitutionsprinzip 

Stellen wir uns vor, Bekannte kommen ausgehungert von einer Wandertour und fragen: »Haste was zu essen?« Die Frage zieht wohl darauf ab, dass es bei Hunger ziemlich egal ist, was wir anbieten, wichtig ist nur etwas Essbares. Daher können wir Eis, aber auch Frittierfett anbieten.
Diese Ausgangslage führt uns zu einem wichtigen Konzept in der Objektorientierung: Wenn wenig gefordert wird, kann mehr angeboten werden. Genauer gesagt: Wenn eine Unterklasse U die Oberklasse O erweitert, können wir überall, wo O gefordert wird, etwa als Parameter einer Funktion, auch ein U übergeben, denn wir werden mit der Unterklasse nur spezieller. Derjenige, dem wir mehr übergeben, kann damit zwar nichts anfangen, aber ablehnen wird er das Objekt nicht, da es alle geforderten Eigenschaften aufweist.
Weil an Stelle eines Objekts auch ein Objekt der Unterklasse auftauchen kann, sprechen wir von Substitution. Das Prinzip wurde von der Professorin Barbara Liskov [Die Zeitschrift »Discover« zählt sie zu den 50 wichtigsten Frauen in der Wissenschaft. Genauso übrigens wie Heidi Hammel, eine Astronomin, die sich dem Jupiter verschrieben hat und sich an bayerischem Starkbier erfreut. ] formuliert und heißt daher auch Liskov’sches Substitutionsprinzip.
Bleiben wir bei unserem Beispiel des Parameters. Für unsere Disko-Vererbungsbeziehung bedeutet dies: Überall dort, wo ein Gebäude gefordert ist, können wir eine Disko übergeben oder auch eine Kirche, wenn Kirche eine Unterklasse von Gebäude ist. Auch können wir eine Kinderdisko eingeben, wenn sie eine Unterklasse von Disko ist. Denn alle diese Dinge sind vom Typ Gebäude und daher typkompatibel.
In der Java-Bibliothek finden sich zahllose weitere Beispiele. Häufigstes Anwendungsfeld sind Datenstrukturen – etwa eine Liste. Die Datenstrukturen nehmen beliebige Objekte entgegen, denn der Parametertyp ist Object – zu sehen etwa an der Methode add(Object) in java.util.ArrayList, der Klasse für Listen. Die Substitution besagt, dass wir alle Objekte dort einsetzen können, da alle Klassen von Object abgeleitet sind.
6.7.7 Automatische und explizite Typanpassung 

Das folgende Beispiel zeigt, dass auch ein Exemplar einer Unterklasse einer Variable vom Typ der Oberklasse zugewiesen werden kann. Wir erzeugen zunächst ein KinderDisko-Objekt:
KinderDisko saloon = new KinderDisko(); Disko d = saloon;
Da eine Kinderdisko eine spezielle Disko ist (KinderDisko als Unterklasse von Disko), funktioniert diese Zuweisung. Auf den ersten Blick erscheint das nicht sonderlich sinnvoll, es erfüllt aber einen Zweck: d übernimmt alle Eigenschaften einer Disko von der mächtigeren Klasse KinderDisko, verzichtet aber auf alle anderen Informationen, die eine Kinderdisko oder sonstige Unterklasse noch bietet, beispielsweise das Attribut maskottchen.
Die Klasse Disko bietet dabei das Attribut anzahlLeute an, sodass auch Folgendes problemlos ist:
System.out.println( d.anzahlLeute );
Versuchen wir jedoch eine spezielle Eigenschaft von KinderDisko zu benutzen, etwa das Attribut maskottchen, so ist dies nicht möglich:
System.out.println( d.maskottchen ); // geht nicht
Hier ist der Typ der Variablen d entscheidend. Der Compiler hat d vom Typ Disko kennen gelernt, daher weiß er nicht, dass d eigentlich ein verkapptes KinderDisko-Objekt ist. Genauso gut lässt sich keine neue Referenz vom Typ KinderDisko auf die Disko legen. Hier gilt wiederum, dass die Typen unvereinbar sind, sodass wir einen Compilerfehler erhalten:
KinderDisko kd = d; // geht nichtEs ist aber möglich, das Objekt d durch eine Typumwandlung in eine KinderDisko umzuwandeln. Dies funktioniert aber lediglich dann, wenn d auch wirklich eine Kinderdisko ist. Dem Compiler ist das in dem Moment egal. Diese Bedingung wird erst zur Laufzeit geprüft:
KinderDisko kd = (KinderDisko) d; // geht wohl
In den beiden folgenden Abschnitten erfahren wir, wieso das sinnvoll und ein mächtiges Konzept ist. Wir werden sehen, dass es möglich ist, eine Basisklasse zu schaffen, und diese verschiedenen Unterklassen Grundfunktionalität beibringen kann. So liefert die Basisklasse einen gemeinsamen Nenner.
Fassen wir die oben stehenden Zeilen in einem kompletten Programm zusammen:
Listing 6.38 v8/DiskoSubstitution.java
package v8; public class DiskoSubstitution { public static void main( String[] args ) { KinderDisko saloon = new KinderDisko(); saloon.maskottchen = "Yosemite Sam"; saloon.personRein(); Disko zumDickenHomer = new KinderDisko(); // zumDickenHomer.maskottchen = "Homer Simpson"; zumDickenHomer.personRein(); Object hase = new KinderDisko(); // hase.maskottchen = "Bugs Bunny"; // hase.personRein(); } }
6.7.8 Typen mit dem binären Operator instanceof testen 

Der relationale Operator instanceof prüft Exemplare auf ihre Verwandtschaft mit einer Klasse (und Schnittstelle oder Aufzählung). Er ist der einzige relationale Operator, der nicht auf numerischen Datentypen operiert, denn er stellt zur Laufzeit fest, ob eine Referenzvariable einen bestimmten Typ besitzt. Dies ist sinnvoll, weil durch objektorientiertes Programmieren laufend Basisobjekte aufgebaut und erweitert werden, und zum Teil verschwindet der Typ für den Compiler, wenn etwa Objekte in allgemeine nicht-typisierte Datenstrukturen gelegt werden.
Listing 6.39 v8/ InstanceofDemo.java, Ausschnitt
boolean b; String str = "Toll"; b = ( str instanceof String ); // wahr b = ( str instanceof Object ); // wahr
Deklariert ist eine Variable str als Objekt vom Typ String. Für den zweiten Fall gilt: Alle Objekte gehen irgendwie aus Object hervor und sind somit logischerweise Erweiterungen.
b = ( str instanceof Date ); // Compilerfehler
Der Ausdruck ist falsch, da Date keine Basisklasse für String ist. Allerdings verweigert der Compiler schon die Übersetzung, da dieser Ausdruck auf keinen Fall passen kann, weil die Vererbungsbeziehungen schon inkompatibel sind.
Die bisherigen Beziehungen hätte der Compiler bereits herausfinden können. Vervollständigen wir das, um zu sehen, dass instanceof wirklich zur Laufzeit den Test machen muss:
KinderDisko saloon = new KinderDisko(); b = ( saloon instanceof KinderDisko ); // ja Disko d = saloon; b = ( d instanceof KinderDisko ); // ja Object o = saloon; b = ( o instanceof Disko ); // ja
Im letzten Fall kennt der Compiler die KinderDisko nur noch als einfaches java.lang.Object Trotzdem erkennt instanceof hinter der Variablen o zur Laufzeit die Disko. Zum Schluss:
Object ref1 = new int[100]; System.out.println( ref1 instanceof String ); // für den Compiler O.K. System.out.println( new int[100] instanceof String );// mag der Compiler nicht
Ein instanceof-Test mit einer Referenz-Variable, die mit null belegt ist, gibt immer false zurück.
Object ref2 = null; System.out.println( ref2 instanceof String ); // für den Compiler O.K. System.out.println( ref2 instanceof null ); // mag der Compiler nicht
6.7.9 Array-Typen und Kovarianz 

Die Aussage »Wer wenig will, kann viel bekommen« gilt auch für Arrays, denn wenn eine Klasse U Unterklasse einer Klasse O ist – und dann gilt Exemplar von U instanceof O –, ist auch U[] ein Untertyp von O[]. Diese Eigenschaft nennt sich Kovarianz. Da Object die Basisklasse aller Objekte ist, kann ein Object-Array auch alle anderen Objekte aufnehmen.
Object[] os = new Object[1]; Disko[] ds = new Disko[1]; Disko d = new Disko(); System.out.println( d instanceof Object ); // true System.out.println( ds instanceof Object[] ); // true os[0] = d;
Bauen wir uns eine Funktion dummGelaufen() und schauen wir, was passiert:
Das Element d soll einfach an die erste Stelle ins Feld gesetzt werden. Rufen wir die Methode mit den soeben deklarierten Variablen os, ds und d auf:
dummGelaufen( ds, d ); dummGelaufen( os, d );
Kein Problem! Die Variable d referenziert ein Disko-Objekt, das sich in einem Disko-Array abspeichern lässt. Der zweite Aufruf funktioniert ebenfalls, denn eine Disko lässt sich in einem Object-Feld speichern, da ja ein Object ein Basistyp ist. Ein Dilemma wäre es jedoch, wenn ein Feld nicht den richtigen Typ bekommt.
dummGelaufen( ds, new Date() );
Das Ergebnis ist eine ArrayStoreException. Das haben wir aber auch verdient, denn ein Date-Objekt lässt sich nicht in einem Disko-Feld speichern. Selbst ein new Object() hätte zu einem Problem geführt. Das Typsystem von Java kann diese Spitzfindigkeit nicht prüfen. Erst zur Laufzeit ist ein Test möglich, mit dem denkbar bitteren Ergebnis einer ArrayStoreException.
6.7.10 Methoden überschreiben 

Wir haben gesehen, dass durch Vererbung eine Unterklasse die sichtbaren Eigenschaften erbt. Die Unterklasse kann nun wiederum Methoden hinzufügen. Dabei ist eine überladene Methode, also eine Funktion, die den gleichen Namen wie die Methode aus einer Oberklasse trägt, aber eine andere Parameteranzahl oder andere -typen hat, eine ganz normale, hinzugefügte Methode.
Eine Unterklasse kann eine Methode aber auch überschreiben. Dazu gibt es in der Unterklasse eine Methode mit der exakten Parameterliste und dem gleichen Methodennamen. Mit anderen Worten: Es existiert in der Unterklasse eine Methode mit der gleichen Signatur wie in der Oberklasse. Zwar gehört der Rückgabetyp nicht zur Signatur, doch muss eine Unterklasse diesen Typ übernehmen. Das gilt immer bei primitiven Typen, bei Referenztypen können es in Unterklassen auch Untertypen sein.
Implementiert die Unterklasse die Methode neu, so sagt sie auf diese Weise: »Ich kann’s besser.« Die überschreibende Methode kann demnach den Funktionscode spezialisieren und Eigenschaften nutzen, die in der Oberklasse nicht bekannt sind. Überladene Funktionen und überschriebene Funktionen sind damit etwas anderes, da eine überladene Funktion mit der Ursprungsfunktion nur »zufällig« den Namen teilt, aber sonst keinen Bezug zur Logik hat.
Altersbeschränkung in der Disko
In eine Disko dürfen nur Personen, die über 18 sind. Eine Methode personRein() soll daher testen, ob Personen Eintritt bekommen. Da KinderDisko von Disko erbt, hätten wir ein Problem, wenn wir personRein() so stehen lassen – dann würde kein Kind mehr in die Kinderdisko kommen! Wir überschreiben daher die Funktion personRein() in der Unterklasse KinderDisko, sodass bereits Personen ab einem Alter von 6 Jahren in die Spezialdisko kommen.
Listing 6.40 Ausschnitt aus v9/Disko.java
public void personRein( int alter ) { if ( alter >= 18 ) anzahlPersonen++; else System.out.println( "Noch zu jung!" ); }
Listing 6.41 Ausschnitt aus v9/KinderDisko.java
public class KinderDisko extends Disko { public String maskottchen; @Override public void personRein( int alter ) { if ( alter >= 6 ) anzahlPersonen++; else System.out.println( "Du bist noch zu klein! Du musst draußen spielen." ); } }
Damit die Unterklasse auf das Attribut anzahlPerson zugreifen kann, machen wir es in der Oberklasse protected. Das UML-Diagramm zeigt es an der Raute.
Abschließend schreiben wir uns noch ein Testprogramm. Wird ein KinderDisko-Objekt angelegt und rufen wir personRein() auf, so wird die überschriebene Funktion verwendet.
public class KiDiDemo { public static void main( String[] args ) { Disko ttwister = new Disko(); ttwister.personRein( 45 ); ttwister.personRein( 10 ); // Noch zu jung! KinderDisko flohzirkus = new KinderDisko(); flohzirkus.personRein( 10 ); flohzirkus.personRein( 5 ); // Du bist noch zu klein! Du musst draußen spielen. } }
Somit bieten sich generell drei Möglichkeiten für Methoden in der Unterklasse an: Hinzufügen, Überladen oder Überschreiben. Wird die Signatur eines Funktionsblocks beim Überschreiben nicht aufmerksam genug beachtet, wird unbeabsichtigt eine Methode überladen. Dieser Fehler ist schwer zu finden.
Die Annotation @Override
Unser Beispiel nutzt die Annotation @Override bei der Methode personRein() – und schon früher bei toString() – und macht auf diese Weise deutlich, dass die Unterklasse eine Methode der Oberklasse überschreibt.
@Override public void personRein( int alter )
Annotationen sind zusätzliche Modifizierer, die entweder vom Compiler überprüft werden oder von uns nachträglich abgefragt werden können. Obwohl wir die Annotation @Override nicht nutzen müssen, hat sie den Vorteil, dass der Compiler überprüft, ob wir tatsächlich eine Methode aus der Oberklasse überschreiben – haben wir uns im Methodennamen verschrieben und würde die Unterklasse auf diese Weise eine neue Methode hinzufügen, so würde der Compiler das als Fehler melden. Fehler wie tostring() fallen schnell auf.
| Hinweis Die Annotation @Override bedeutet nicht, dass diese Methode in Unterklassen überschrieben werden muss, sondern nur, dass sie selbst eine Methode überschreibt. |
6.7.11 Mit super eine Methode der Oberklasse aufrufen 

Wenn wir eine Methode überschreiben, dann entscheiden wir uns für eine gänzlich neue Implementierung. Was ist aber, wenn die Funktionalität im Großen und Ganzen gut war und nur eine Kleinigkeit fehlte? In diesem Fall kann mit der Referenz super auf eine Eigenschaft der Oberklasse verwiesen werden.
class PrahlerOber { int i = 1; void m() { System.out.println( "Ich bin toll" ); } } class PrahlerUnter extends PrahlerOber { int i = 2; @Override void m() { super.m(); System.out.println( "Und ich bin noch toller" ); System.out.println( super.i ); System.out.println( i ); } }
Die Methode m() aus PrahlerUnter bezieht sich mittels super.m() auf die Methode der Oberklasse. Im zweiten Fall können wir auf die verdeckte Objektvariable mit super.i zugreifen. super ist vergleichbar mit this und kann auch genauso eingesetzt werden, doch super geht in den Namensraum der Oberklasse. Natürlich ist das Objekt hinter super und this das gleiche.
Wir rufen zwar in m() die überschriebene Methode auf, benötigen aber dafür ihren Namen. Interessant wäre eine Variante von super – die es aber nicht gibt –, mit der der Methodenname egal ist. So muss bei einer Änderung des Namens der Quellcode an zwei Stellen angepasst werden. Ein super() für die Oberklasse existiert nur für Konstruktoren.
Eine Aneinanderreihung von super-Schlüsselwörtern bei einer tieferen Vererbungshierarchie ist nicht möglich. Hinter einem super muss eine Objekteigenschaft stehen. Anweisungen wie super.super.i sind somit immer ungültig. Für Variablen gibt es jedoch eine Möglichkeit, die sich durch einen Cast in die Oberklasse ergibt. Wir erfinden eine neue Unterklasse Oberangeber, die wiederum von PrahlerUnter erbt.
class Oberangeber extends PrahlerUnter { @Override void m() { System.out.println( ((PrahlerOber)this).i ); } }
Die this-Referenz entspricht einem Objekt vom Typ Oberangeber. Wenn wir dies aber in den Typ PrahlerOber konvertieren, bekommen wir genau das i aus der Basisklasse unserer Hierarchie. Wir erkennen hier eine sehr wichtige Eigenschaft von Java, nämlich, dass Variablen nicht dynamisch gebunden werden. Anders wäre es, wenn wir die Funktion ändern in:
void m() { ((PrahlerOber)this).m(); }
Hier wird nicht wie erwartet m() aus PrahlerOber aufgerufen, sondern die eigene Funktion m(), so dass wir in einer Rekursion landen. Der Grund dafür liegt in der dynamischen Bindung, was in Kürze im Abschnitt »Die Oberklasse gibt Funktionalität vor« erläutert.
6.7.12 Kovariante Rückgabetypen 

Seit Java 5 kann eine Unterklasse eine Methode mit nichtprimitivem Rückgabetyp überschreiben, die einen anderen Rückgabetyp besitzt, nämlich den der Unterklasse. Das nennt sich kovarianter Rückgabetyp und ist sehr praktisch, da sich auf diese Weise Entwickler oft explizite Typanpassungen sparen können.
Um das zu zeigen, ein Beispiel. Die Klasse Lautsprecher deklariert eine Methode gibThis(), die lediglich die this-Referenz zurückgibt. Eine Unterklasse überschreibt die Methode und liefert einen spezielleren Typ.
Listing 6.44 FetterBassLautsprecher.java
class Lautsprecher { Lautsprecher gibThis() { return this; } } class FetterBassLautsprecher extends Lautsprecher { @Override FetterBassLautsprecher gibThis() // Lautsprecher gibThis() { return this; } }
Merkwürdig in diesem Zusammenhang ist, dass es schon immer in Java veränderte Zugriffsrechte gegeben hat. Eine Unterklasse kann die Sichtbarkeit erweitern. Auch bei Ausnahmen kann eine Unterklasse speziellere Ausnahmen beziehungsweise ganz andere Ausnahmen als die Methode der Oberklasse erzeugen.
6.7.13 Finale Klassen 

Soll eine Klasse keine Unterklassen bilden, werden Klassen mit dem Modifizierer final versehen. Dadurch kann vermieden werden, dass Unterklassen Eigenschaften nachträglich verändern können. Ein Versuch, von einer finalen Klasse zu erben, führt zu einem Compilerfehler. Dies schränkt zwar die objektorientierte Wiederverwendung ein, wird aber aufgrund von Sicherheitsaspekten in Kauf genommen. Eine Passwortüberprüfung soll zum Beispiel nicht einfach überschrieben werden können.
In der Java-Bibliothek gibt es eine Reihe von finalen Klassen, von denen wir einige bereits kennen gelernt haben:
- String, StringBuffer, StringBuilder
- Wrapper-Klassen
- Math
- System
- Font, Color
6.7.14 Nicht überschreibbare Funktionen 

In der Vererbungshierarchie möchte ein Designer in manchen Fällen verhindern, dass Unterklassen eine Methode überschreiben und mit neuer Logik implementieren. Das verhindert der zusätzliche Modifizierer final an der Methodendeklaration. Da Methodenaufrufe immer dynamisch gebunden werden, könnte ein Aufrufer unbeabsichtigt in der Unterklasse landen, was finale Methoden vermeiden.






